/*
 * ==========================================================================*\
 * | $Id$
 * |*-------------------------------------------------------------------------*|
 * | Copyright (C) 2009 Virginia Tech | | This file is part of CloudSpace. | |
 * CloudSpace is free software; you can redistribute it and/or modify | it under
 * the terms of the GNU General Public License as published | by the Free
 * Software Foundation; either version 3 of the License, or | (at your option)
 * any later version. | | CloudSpace is distributed in the hope that it will be
 * useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of |
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General
 * Public License for more details. | | You should have received a copy of the
 * GNU General Public License | along with CloudSpace; if not, see
 * <http://www.gnu.org/licenses/>.
 * \*==========================================================================
 */

package cloudspace.security.permissions;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.StringUtils;

import cloudspace.config.CloudSpaceConfiguration;
import cloudspace.security.CloudSpaceUser;
import cloudspace.security.GroupFactory;
import cloudspace.vm.filesystem.CSPath;


// -------------------------------------------------------------------------
/**
 * A singleton that provides operations to manage user groups on CloudSpace and
 * the path-based access to resources on the file system for users and groups.
 * 
 * @author Tony Allevato
 * @author Last changed by $Author$
 * @version $Revision$, $Date$
 */
public class PermissionsManager
{
    // ~ Constructors ..........................................................

    // ----------------------------------------------------------
    private PermissionsManager()
    {
        groupsToUsersMap = new HashMap<GrantedAuthority, Set<String>>();
        directoriesToPermissionsMap = new HashMap<CSPath, List<PermissionToken>>();

        try
        {
            load();
        }
        catch ( FileNotFoundException e )
        {
            // Ignore this -- a single user system may not have this file
            // created yet.
        }
        catch ( IOException e )
        {
            log.error( "An exception occurred while reading permissions.conf ",
                e );
        }
    }


    // ~ Methods ...............................................................

    // ----------------------------------------------------------
    public static PermissionsManager getInstance()
    {
        if ( instance == null )
        {
            instance = new PermissionsManager();
        }

        return instance;
    }


    // ----------------------------------------------------------
    private synchronized void load() throws IOException
    {
        CloudSpaceConfiguration config = CloudSpaceConfiguration.getInstance();
        File configFile = config.getFileInStorage( PERMISSIONS_CONFIG_FILE );

        BufferedReader reader = new BufferedReader( new FileReader( configFile ) );
        String line = reader.readLine();
        CSPath dir = null;
        int parseState = 0;

        final int STATE_GROUPS = 1;
        final int STATE_PATH = 2;

        while ( line != null )
        {
            line = line.trim();

            if ( line.startsWith( "#" ) )
            {
                // Comment; ignore it.
            }
            else
            {
                Matcher matcher;

                if ( ( matcher = SECTION_PATTERN.matcher( line ) ) != null
                    && matcher.matches() )
                {
                    String sectionName = matcher.group( 1 );

                    if ( sectionName.equals( "groups" ) )
                    {
                        parseState = STATE_GROUPS;
                    }
                    else
                    {
                        parseState = STATE_PATH;

                        String path = matcher.group( 1 );
                        dir = new CSPath( path );

                        directoriesToPermissionsMap.put( dir,
                            new ArrayList<PermissionToken>() );
                    }
                }
                else if ( line.length() > 0 )
                {
                    if ( parseState == STATE_GROUPS )
                    {
                        processGroupLine( line );
                    }
                    else if ( parseState == STATE_PATH )
                    {
                        processPathAccessLine( dir, line );
                    }
                }
            }

            line = reader.readLine();
        }
    }


    // ----------------------------------------------------------
    private void processGroupLine( String line )
    {
        String[] parts = line.split( "=" );
        String groupName = parts[0].trim();
        String memberList = parts[1];

        String[] members = memberList.split( "," );

        GrantedAuthority group = GroupFactory.groupWithName( groupName );
        HashSet<String> groupMembers = new HashSet<String>();
        groupsToUsersMap.put( group, groupMembers );

        for ( String member : members )
        {
            member = member.trim();

            // TODO Add proper support for nested groups. Need a solution that
            // will be correctly re-serialized back to the config file.
            /*
             * if (member.startsWith("@")) { Set<String> subgroupMembers =
             * groupsToUsersMap.get(member.substring(1));
             * groupMembers.addAll(subgroupMembers); } else
             */
            {
                groupMembers.add( member );
            }
        }
    }


    // ----------------------------------------------------------
    private void processPathAccessLine( CSPath directory, String line )
    {
        String[] parts = line.split( "=" );
        String entity = parts[0].trim();
        String permissionString = parts[1].trim();

        if ( entity.equals( "*" ) )
        {
            directoriesToPermissionsMap.get( directory )
                .add( new AnybodyPermissionToken( permissionString ) );
        }
        else if ( entity.startsWith( "@" ) )
        {
            String groupName = entity.substring( 1 );
            GrantedAuthority group = GroupFactory.groupWithName( groupName );

            directoriesToPermissionsMap.get( directory )
                .add( new GroupPermissionToken( group, permissionString ) );
        }
        else
        {
            directoriesToPermissionsMap.get( directory )
                .add( new UserPermissionToken( entity, permissionString ) );
        }
    }


    // ----------------------------------------------------------
    /* package */synchronized void updateConfigFile()
    {
        PrintWriter writer = null;

        try
        {
            CloudSpaceConfiguration config = CloudSpaceConfiguration.getInstance();
            File configFile = config.getFileInStorage( PERMISSIONS_CONFIG_FILE,
                true );

            writer = new PrintWriter( configFile );

            writer.println( "[groups]" );

            for ( GrantedAuthority group : groupsToUsersMap.keySet() )
            {
                Set<String> users = groupsToUsersMap.get( group );

                writer.printf( "%s = %s\n",
                    group.getAuthority(),
                    StringUtils.collectionToCommaDelimitedString( users ) );
            }

            writer.println();

            for ( CSPath directory : directoriesToPermissionsMap.keySet() )
            {
                writeDirectoryToConfigFile( writer, directory );
            }
        }
        catch ( IOException e )
        {
            log.error( "An exception occurred while writing the directories "
                + "configuration file", e );
        }
        finally
        {
            if ( writer != null )
            {
                writer.close();
            }
        }
        this.usersOwningPathCache.clear();
    }


    // ----------------------------------------------------------
    private void writeDirectoryToConfigFile(
        PrintWriter writer,
        CSPath directory ) throws IOException
    {
        List<PermissionToken> permissions = directoriesToPermissionsMap.get( directory );

        writer.printf( "[%s:%s]\n", directory.getZone(), directory.getPath() );

        for ( PermissionToken permission : permissions )
        {
            writer.println( permission.toString() );
        }

        writer.println();
    }


    // ----------------------------------------------------------
    public synchronized void addGroup( GrantedAuthority group )
    {
        if ( !groupsToUsersMap.containsKey( group ) )
        {
            HashSet<String> members = new HashSet<String>();
            groupsToUsersMap.put( group, members );

            updateConfigFile();
        }
    }


    // ----------------------------------------------------------
    public synchronized void deleteGroup( GrantedAuthority group )
    {
        groupsToUsersMap.remove( group );

        updateConfigFile();
    }


    // ----------------------------------------------------------
    public synchronized void addUserToGroup( String user, GrantedAuthority group )
    {
        Set<String> members = groupsToUsersMap.get( group );

        if ( members == null )
        {
            members = new HashSet<String>();
            groupsToUsersMap.put( group, members );
        }

        if ( !members.contains( user ) )
        {
            members.add( user );

            updateConfigFile();
        }
    }


    // ----------------------------------------------------------
    public synchronized void removeUserFromGroup(
        String user,
        GrantedAuthority group )
    {
        Set<String> members = groupsToUsersMap.get( group );
        members.remove( user );

        updateConfigFile();
    }


    // ----------------------------------------------------------
    public synchronized List<GrantedAuthority> getGroupList()
    {
        List<GrantedAuthority> groups = new ArrayList<GrantedAuthority>( groupsToUsersMap.keySet() );

        Collections.sort( groups, new Comparator<GrantedAuthority>()
        {
            public int compare( GrantedAuthority o1, GrantedAuthority o2 )
            {
                return o1.getAuthority().compareTo( o2.getAuthority() );
            }
        } );

        return groups;
    }


    // ----------------------------------------------------------
    public synchronized List<String> getGroupMembers( GrantedAuthority group )
    {
        Set<String> members = groupsToUsersMap.get( group );
        List<String> memberList = new ArrayList<String>( members );
        Collections.sort( memberList );
        return memberList;
    }


    // ----------------------------------------------------------
    public synchronized boolean isUserInGroup(
        String user,
        GrantedAuthority group )
    {
        Set<String> members = groupsToUsersMap.get( group );
        return members.contains( user );
    }


    // ----------------------------------------------------------
    public synchronized boolean isUserAdmin( String user )
    {
        return isUserInGroup( user, GroupFactory.ADMIN_GROUP );
    }


    // ----------------------------------------------------------
    public synchronized void addDirectoryPermission(
        CSPath directory,
        PermissionToken permission )
    {
        List<PermissionToken> permissions = directoriesToPermissionsMap.get( directory );

        if ( permissions == null )
        {
            permissions = new ArrayList<PermissionToken>();
            directoriesToPermissionsMap.put( directory, permissions );
        }

        // TODO how should we handle the case where the authority already
        // exists?

        Iterator<PermissionToken> it = permissions.iterator();
        while ( it.hasNext() )
        {
            PermissionToken currentPermission = it.next();

            if ( currentPermission.isSimilarTo( permission ) )
            {
                it.remove();
            }
        }

        permissions.add( permission );

        updateConfigFile();
    }


    // ----------------------------------------------------------
    public synchronized void removeUserDirectoryPermission(
        CSPath directory,
        String username )
    {
        List<PermissionToken> permissions = directoriesToPermissionsMap.get( directory );

        if ( permissions != null )
        {
            boolean removed = false;

            Iterator<PermissionToken> it = permissions.iterator();
            while ( it.hasNext() )
            {
                PermissionToken permission = it.next();

                if ( permission instanceof UserPermissionToken )
                {
                    UserPermissionToken token = (UserPermissionToken)permission;

                    if ( token.getUsername().equals( username ) )
                    {
                        removed = true;
                        it.remove();
                    }
                }
            }

            if ( permissions.isEmpty() )
            {
                directoriesToPermissionsMap.remove( directory );
            }

            if ( removed )
            {
                updateConfigFile();
            }
        }
    }


    // ----------------------------------------------------------
    public synchronized void removeGroupDirectoryPermission(
        CSPath directory,
        GrantedAuthority group )
    {
        List<PermissionToken> permissions = directoriesToPermissionsMap.get( directory );

        if ( permissions != null )
        {
            boolean removed = false;

            Iterator<PermissionToken> it = permissions.iterator();
            while ( it.hasNext() )
            {
                PermissionToken permission = it.next();

                if ( permission instanceof GroupPermissionToken )
                {
                    GroupPermissionToken token = (GroupPermissionToken)permission;

                    if ( token.getGroup().equals( group ) )
                    {
                        removed = true;
                        it.remove();
                    }
                }
            }

            if ( permissions.isEmpty() )
            {
                directoriesToPermissionsMap.remove( directory );
            }

            if ( removed )
            {
                updateConfigFile();
            }
        }
    }

    private Map<String, List<String>> usersOwningPathCache = new HashMap<String, List<String>>();


    // ----------------------------------------------------------
    public synchronized List<String> lookupUsers( CSPath directory )
    {
        String fullFilePath = directory.getPhysicalFile().getAbsolutePath();
        if ( usersOwningPathCache.containsKey( fullFilePath ) )
            return usersOwningPathCache.get( fullFilePath );
        List<PermissionToken> tokens = directoriesToPermissionsMap.get( directory );
        while ( tokens == null && directory.getParent() != null )
        {
            directory = directory.getParent();
            tokens = directoriesToPermissionsMap.get( directory );
        }
        if ( tokens == null )
        {
            usersOwningPathCache.put( fullFilePath, null );
            return null;
        }
        else
        {
            List<String> userTokens = new ArrayList<String>();
            for ( PermissionToken token : tokens )
            {
                if ( token instanceof UserPermissionToken )
                {
                    userTokens.add( ( (UserPermissionToken)token ).getUsername() );
                }
            }
            usersOwningPathCache.put( fullFilePath, userTokens );
            return userTokens;
        }

    }


    // ----------------------------------------------------------
    public synchronized PermissionToken lookupPermission(
        CSPath directory,
        CloudSpaceUser user )
    {
        if ( isUserAdmin( user.getUsername() ) )
        {
            return new AnybodyPermissionToken( true, true );
        }
        PermissionToken perm;
        while ( directory != null )
        {
            perm = checkDirectoryPermission( directory, user );
            if ( perm != null )
            {
                return perm;
            }
            directory = directory.getParent();
        }

        return NoAccessPermissionToken.getInstance();
    }


    private PermissionToken checkDirectoryPermission(
        CSPath directory,
        CloudSpaceUser user )
    {
        List<PermissionToken> tokens = directoriesToPermissionsMap.get( directory );

        if ( tokens != null )
        {
            TreeSet<PermissionToken> matchingTokens = new TreeSet<PermissionToken>( new PermissionTokenSpecificityComparator() );

            for ( PermissionToken token : tokens )
            {
                if ( token.matchesUser( user ) )
                {
                    matchingTokens.add( token );
                }
            }

            if ( !matchingTokens.isEmpty() )
            {
                return matchingTokens.pollLast();
            }
        }
        return null;
    }


    // ----------------------------------------------------------
    public synchronized List<CSPath> getAdminRoots()
    {
        List<CSPath> roots = new ArrayList<CSPath>();

        roots.add( new CSPath( "work:/" ) );
        roots.add( new CSPath( "sample", "/" ) );
        roots.add( new CSPath( "snapshot:/" ) );
        return roots;
    }


    // ----------------------------------------------------------
    public synchronized List<CSPath> getUserRoots( CloudSpaceUser user )
    {
        // If the user is in the "admin" group, we just give them implicit
        // access to the system root and ignore any other permissions.

        if ( isUserAdmin( user.getUsername() ) )
        {
            return getAdminRoots();
        }

        List<CSPath> roots = new ArrayList<CSPath>();

        for ( CSPath directory : directoriesToPermissionsMap.keySet() )
        {
            PermissionToken permission = checkDirectoryPermission( directory,
                user );
            if ( permission == null )
            {
                permission = NoAccessPermissionToken.getInstance();
            }
            if ( permission.allowsRead() || permission.allowsWrite() )
            {
                // TODO check if this directory is a prefix or a suffix of any
                // others that have already been added; if so, manipulate the
                // list accordingly

                roots.add( directory );
            }
        }

        Collections.sort( roots );
        return roots;
    }


    // ----------------------------------------------------------
    public synchronized Map<CSPath, PermissionToken> getUserPermissions(
        String username )
    {
        Map<CSPath, PermissionToken> perms = new TreeMap<CSPath, PermissionToken>();

        for ( CSPath dir : directoriesToPermissionsMap.keySet() )
        {
            List<PermissionToken> permissions = directoriesToPermissionsMap.get( dir );

            for ( PermissionToken permission : permissions )
            {
                if ( permission instanceof UserPermissionToken )
                {
                    UserPermissionToken utoken = (UserPermissionToken)permission;

                    if ( utoken.getUsername().equals( username ) )
                    {
                        perms.put( dir, permission );
                        break;
                    }
                }
            }
        }

        return perms;
    }


    // ----------------------------------------------------------
    public synchronized Map<CSPath, PermissionToken> getGroupPermissions(
        GrantedAuthority group )
    {
        Map<CSPath, PermissionToken> perms = new TreeMap<CSPath, PermissionToken>();

        for ( CSPath dir : directoriesToPermissionsMap.keySet() )
        {
            List<PermissionToken> permissions = directoriesToPermissionsMap.get( dir );

            for ( PermissionToken permission : permissions )
            {
                if ( permission instanceof GroupPermissionToken )
                {
                    GroupPermissionToken gtoken = (GroupPermissionToken)permission;

                    if ( gtoken.getGroup().equals( group ) )
                    {
                        perms.put( dir, permission );
                        break;
                    }
                }
            }
        }

        return perms;
    }


    // ----------------------------------------------------------
    public synchronized CSPath lookupDirectory( String path )
    {
        // TODO is there a need to provide a faster map-based lookup for this
        // instead of a linear search?

        for ( CSPath dir : directoriesToPermissionsMap.keySet() )
        {
            if ( dir.getPath().equals( path ) )
            {
                return dir;
            }
        }

        return null;
    }


    // ----------------------------------------------------------
    public synchronized void reload()
    {
        try
        {
            load();
        }
        catch ( FileNotFoundException e )
        {
            // It's possible for the file not to be found if we are running
            // as a single-user system. Just ignore this.
        }
        catch ( IOException e )
        {
            throw new AccessControlException( "Cannot read the directory configuration file; "
                + "contact administrator" );
        }
    }

    // ~ Static/instance variables .............................................

    private static PermissionsManager instance;

    private static final String PERMISSIONS_CONFIG_FILE = "conf/permissions.conf";

    private static final Pattern SECTION_PATTERN = Pattern.compile( "\\[(.*)\\]" );

    private Map<GrantedAuthority, Set<String>> groupsToUsersMap;

    private Map<CSPath, List<PermissionToken>> directoriesToPermissionsMap;

    private static final Logger log = Logger.getLogger( PermissionsManager.class );
}
